﻿namespace Home.Services.SecurityService
{
    using System;
    using System.Data.Entity;
    using System.Dynamic;
    using System.Linq;
    using System.Net;
    using System.Net.Http;
    using System.Security.Claims;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Web.Http;
    using System.Web.Http.Controllers;
    using System.Web.OData;
    using System.Web.OData.Query;
    using System.Web.OData.Results;
    using System.Web.OData.Routing;
    using Home.Configuration;
    using Home.Data;

    [OutputCache(NoCache = true)]
    [ServiceHandleError(ServiceName = ClientServiceHandleErrorHelper.SecurityServiceName)]
    public class UsersController : BaseODataController
    {
        private readonly SecurityDbContext database = new SecurityDbContext();

        // GET odata/Users
        [ClientServiceAuthorize]
        [HttpGet]
        [EnableQuery(PageSize = 50)]
        [ODataRoute("Users")]
        public IHttpActionResult Get()
        {
            var users = this.database.Users;
            return Ok(users);
        }

        // GET odata/Users('id')
        [ClientServiceAuthorize]
        [HttpGet]
        [EnableQuery]
        [ODataRoute("Users({id})")]
        public Task<IHttpActionResult> Get([FromODataUri] Guid id, ODataQueryOptions<User> options)
        {
            var users = this.database.Users.Where(r => r.Id == id);
            return GetODataSingleAsync(users, options);
        }

        // GET odata/Users('id')/Property
        [ClientServiceAuthorize]
        [HttpGet]
        [ODataRoute("Users({id})/LoginName")]
        [ODataRoute("Users({id})/DisplayName")]
        [ODataRoute("Users({id})/Role")]
        [ODataRoute("Users({id})/IsLocked")]
        [ODataRoute("Users({id})/LockDateTime")]
        [ODataRoute("Users({id})/Created")]
        [ODataRoute("Users({id})/CreatedBy")]
        [ODataRoute("Users({id})/Modified")]
        [ODataRoute("Users({id})/ModifiedBy")]
        public async Task<IHttpActionResult> GetProperty([FromODataUri] Guid id)
        {
            var user = await this.database.Users.FirstOrDefaultAsync(r => r.Id == id);
            return GetODataProperty(user);
        }

        // POST odata/Users
        [ClientServiceAuthorize(Roles = SecurityServiceHelper.RoleConstants.Manager)]
        [HttpPost]
        [ODataRoute("Users")]
        public async Task<IHttpActionResult> Post(User user)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            if (!ConfigurationHelper.IsUnitTestEnvironment && !ConfigurationHelper.IsE2ETestEnvironment && Thread.CurrentPrincipal.CompareRole(user.Role) < 0)
            {
                return StatusCode(HttpStatusCode.Unauthorized);
            }

            return Created(await CreateAsync(user));
        }

        // PUT odata/Users('id')
        [ClientServiceAuthorize(Roles = SecurityServiceHelper.RoleConstants.Manager)]
        [HttpPut]
        [ODataRoute("Users({id})")]
        public async Task<IHttpActionResult> Put([FromODataUri] Guid id, User user, ODataQueryOptions<User> options)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            if (id != user.Id)
            {
                return BadRequest("The Id of user does not match the id");
            }

            var value = await CheckPermissionAsync(id, false);

            if (value.Result != null)
            {
                return value.Result;
            }

            var users = this.database.Users.Where(r => r.Id == id);

            return await PutOrPatchODataAsync(users, options,
                () => CreateAsync(user),
                () => UpdateAsync(user));
        }

        // PATCH odata/Users('id')
        [ClientServiceAuthorize(Roles = SecurityServiceHelper.RoleConstants.Manager)]
        [HttpPatch]
        [ODataRoute("Users({id})")]
        public async Task<IHttpActionResult> Patch([FromODataUri] Guid id, Delta<User> patch, ODataQueryOptions<User> options)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            var value = await CheckPermissionAsync(id, false);

            if (value.Result != null)
            {
                return value.Result;
            }

            var users = this.database.Users.Where(r => r.Id == id);

            return await PutOrPatchODataAsync(users, options,
                () =>
                {
                    return CreatePatchAsync(patch);
                },
                async () =>
                {
                    return await UpdatePatchAsync(await users.FirstAsync(), patch);
                });
        }

        // DELETE odata/Users('id')
        [ClientServiceAuthorize(Roles = SecurityServiceHelper.RoleConstants.Manager)]
        [HttpDelete]
        [ODataRoute("Users({id})")]
        public async Task<IHttpActionResult> Delete([FromODataUri] Guid id, ODataQueryOptions<User> options)
        {
            var value = await CheckPermissionAsync(id, true);

            if (value.Result != null)
            {
                return value.Result;
            }

            var users = this.database.Users.Where(r => r.Id == id);

            return value.Result != null ? value.Result : await DeleteODataAsync(users, options, () => users.First().DeleteAsync(this.database, this.database.Users));
        }

        [ClientServiceAuthorize]
        [HttpPost]
        [ODataRoute("Users({id})/Home.Services.SecurityService.ChangePassword()")]
        public async Task<IHttpActionResult> ChangePassword([FromODataUri] Guid id, ODataActionParameters parameters)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            var oldPassword = (string)parameters["old"];
            var newPassword = (string)parameters["new"];
            if (string.IsNullOrWhiteSpace(oldPassword) || string.IsNullOrWhiteSpace(newPassword) || newPassword.Length < SecurityServiceHelper.MinPasswordLength || newPassword.Length > SecurityServiceHelper.MaxPasswordLength)
            {
                return BadRequest(ModelState);
            }

            var user = await this.database.Users.FirstOrDefaultAsync(r => r.Id == id);
            if (user == null)
            {
                return NotFound();
            }

            if (!ConfigurationHelper.IsUnitTestEnvironment && !ConfigurationHelper.IsE2ETestEnvironment)
            {
                // only can modify self password
                if (user.Id != new Guid(((ClaimsIdentity)Thread.CurrentPrincipal.Identity).FindFirst(ClaimTypes.NameIdentifier).Value))
                {
                    return StatusCode(HttpStatusCode.Unauthorized);
                }
            }

            if (user._Password != oldPassword)
            {
                return StatusCode(HttpStatusCode.PreconditionFailed);
            }

            user._Password = newPassword;
            user.Modified = DateTimeOffset.Now;
            user.ModifiedBy = Thread.CurrentPrincipal.Identity.Name;
            await this.database.SaveChangesAsync();

            return Ok();
        }

        [ClientServiceAuthorize(Roles = SecurityServiceHelper.RoleConstants.Manager)]
        [HttpPost]
        [ODataRoute("Users({id})/Home.Services.SecurityService.UpdatePassword()")]
        public async Task<IHttpActionResult> UpdatePassword([FromODataUri] Guid id, ODataActionParameters parameters)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            var newPassword = (string)parameters["new"];
            if (string.IsNullOrWhiteSpace(newPassword) || newPassword.Length < SecurityServiceHelper.MinPasswordLength || newPassword.Length > SecurityServiceHelper.MaxPasswordLength)
            {
                return BadRequest(ModelState);
            }

            var value = await CheckPermissionAsync(id, true);

            if (value.Result != null)
            {
                return value.Result;
            }

            value.User._Password = newPassword;
            value.User.Modified = DateTimeOffset.Now;
            value.User.ModifiedBy = Thread.CurrentPrincipal.Identity.Name;
            await this.database.SaveChangesAsync();

            return Ok();
        }

        [ClientServiceAuthorize(Roles = SecurityServiceHelper.RoleConstants.Manager)]
        [HttpGet]
        [ODataRoute("Users({id})/Home.Services.SecurityService.GetPassword()")]
        public async Task<IHttpActionResult> GetPassword([FromODataUri] Guid id)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            var value = await CheckPermissionAsync(id, true);

            return value.Result != null ? value.Result : Ok(new Password() { Value = value.User._Password });
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                this.database.Dispose();
            }

            base.Dispose(disposing);
        }

        private async Task<User> CreateAsync(User entity)
        {
            var now = DateTimeOffset.UtcNow;
            var user = Thread.CurrentPrincipal.Identity.Name;

            //// default password
            entity._Password = entity.GetDefaultPassword();
            //// update lock state
            if (entity.IsLocked)
            {
                entity.LockDateTime = now;
            }

            entity.Create(this.database.Users, now, user, true);
            await this.database.SaveChangesAsync();
            return entity;
        }

        private async Task<User> UpdateAsync(User entity)
        {
            var now = DateTimeOffset.UtcNow;
            var user = Thread.CurrentPrincipal.Identity.Name;

            var current = await this.database.Users.FindAsync(entity.Id);
            this.database.Entry(current).State = EntityState.Detached;
            //// get password for update
            entity._Password = current._Password;
            //// update lock state
            if (entity.IsLocked && !current.IsLocked)
            {
                entity.LockDateTime = now;
            }

            entity.Update(this.database, this.database.Users, now, user);
            await this.database.SaveChangesAsync();
            return entity;
        }

        private Task<User> CreatePatchAsync(Delta<User> patch)
        {
            var entity = new User();
            patch.Patch(entity);
            return CreateAsync(entity);
        }

        private Task<User> UpdatePatchAsync(User entity, Delta<User> patch)
        {
            patch.Patch(entity);
            return UpdateAsync(entity);
        }

        private async Task<CheckPermissionResult> CheckPermissionAsync(Guid id, bool handleNotFound)
        {
            var result = new CheckPermissionResult();

            result.User = await this.database.Users.FirstOrDefaultAsync(r => r.Id == id);
            if (result.User == null)
            {
                if (handleNotFound)
                {
                    result.Result = NotFound();
                }
            }
            else if (!ConfigurationHelper.IsUnitTestEnvironment && !ConfigurationHelper.IsE2ETestEnvironment && Thread.CurrentPrincipal.CompareRole(result.User.Role) < 0)
            {
                result.Result = StatusCode(HttpStatusCode.Unauthorized);
            }

            return result;
        }

        private class CheckPermissionResult
        {
            public IHttpActionResult Result { get; set; }
            public User User { get; set; }
        }
    }
}
